import os
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Optional, List

from werkzeug.datastructures import FileStorage

from app.db.crud.teacher_management_query import course_query, user_query
from app.db.model import TeachingMaterialSubmit
from app.utils.exceptions import ResourceNotFoundError, InternalServerError
from app.envConfig import ENV_CONFIG


class Files:
    # 类属性，用于存储延迟加载的映射

    def upload_file(self, file: FileStorage, file_path: str, file_name: str) -> None:

        # 1. 构建目标路径，并标准化
        # resolve()返回绝对路径
        upload_path = Path(file_path).resolve()

        # 2. 如果目录不存在，则创建
        if not upload_path.exists():
            upload_path.mkdir(parents=True, exist_ok=True)

        # 3. 构建完整文件路径
        target_path = upload_path / file_name

        # 4. 保存文件
        try:
            file.save(target_path)
        except Exception as e:
            raise IOError(f"文件上传失败: {e}")

    def get_tempdir_file_path(self, semester: str, dept_name: str, material_type: str,
                              course_name: str, class_name: str, teacher_name: str) -> str:
        """
        获取文件存储路径
        Args:
            semester (str): 学期
            dept_name (str): 部门名称
            material_type (str): 材料类型
            course_name (str): 课程名称
            class_name (str): 班级名称
            teacher_name (str): 教师姓名
        Returns:
            str: 文件夹路径
        """
        material_map = ENV_CONFIG.material_type_mapping.get(material_type)
        if material_type == "teaching_plan":
            file_dir = f"《{course_name}》_{class_name}_{teacher_name}"
            return str(Path(ENV_CONFIG.TEMP_PATH, semester, dept_name, material_map, file_dir))
        return str(Path(ENV_CONFIG.TEMP_PATH, semester, dept_name, material_map))

    def get_workdir_file_path(self, semester: str, dept_name: str, material_type: str,
                              course_name: str, class_name: str, teacher_name: str) -> str:
        """

        :param semester:
        :param dept_name:
        :param material_type:
        :param course_name:
        :param class_name:
        :param teacher_name:
        :return:
        """
        material_map = ENV_CONFIG.material_type_mapping.get(material_type)
        if material_type == "teaching_plan":
            file_dir = f"《{course_name}》教案_{class_name}_{teacher_name}"
            return str(Path(ENV_CONFIG.BASE_PATH, semester, dept_name, material_map, file_dir))
        return str(Path(ENV_CONFIG.BASE_PATH, semester, dept_name, material_map))

    def get_file_name(self, material_type: str, semester: str, course_name: str, class_name: str,
                      teacher_name: str, week: int, file_extension: str) -> str:
        """

        :param material_type:
        :param semester:
        :param course_name:
        :param class_name:
        :param teacher_name:
        :param week:
        :param file_extension:
        :return:
        """
        if material_type == "teaching_plan":
            return f"{semester}教案《{course_name}》_{class_name}_{teacher_name}_第{week}周.{file_extension}"
        elif material_type == "teaching_situation":
            return f"{semester}教师教学工作情况考核表_{teacher_name}.{file_extension}"
        elif material_type == "score_report":
            return f"{semester}课程成绩报告单及教学质量分析表《{course_name}》_{class_name}_{teacher_name}.{file_extension}"
        elif material_type == "training_plan":
            return f"{semester}实训计划《{course_name}》_{class_name}_{teacher_name}.{file_extension}"
        elif material_type == "lesson_plan":
            return f"{semester}授课计划《{course_name}》_{class_name}_{teacher_name}.{file_extension}"
        else:
            raise Exception("文件类型错误")

    def get_week_by_file_name(self, file_name: str) -> Optional[int]:
        """
        从文件名中提取第几周

        Args:
            file_name (str): 文件名

        Returns:
            Optional[int]: 提取出的周次，如果找不到返回 None
        """
        pattern = re.compile(r"第(\d+)周")
        matcher = pattern.search(file_name)
        if matcher:
            return int(matcher.group(1))
        return None

    def check_teaching_plan_folder_name(
            self,
            folder_name: str,
            errors: List[Exception]
    ) -> Optional[List[str]]:
        """
        检查教案文件夹名称格式是否正确，提取课程、班级、教师信息

        Args:
            folder_name (str): 文件夹名，例如《课程名》教案_班级_教师
            errors (List[Exception]): 错误收集列表

        Returns:
            Optional[List[str]]: [课程名, 班级, 教师名]，如果匹配失败返回 None
        """
        regex = r"^《(.*?)》教案_(.*?)_(.*?)$"
        matcher = re.match(regex, folder_name)

        if not matcher:
            # 分段检查错误类型
            if not self.test_regex_match(r"^《.*?》教案_", folder_name):
                errors.append(Exception("课程识别失败", folder_name))
            elif not self.test_regex_match(r"》教案_.*?_", folder_name):
                errors.append(Exception("班级识别失败", folder_name))
            elif not self.test_regex_match(r"_.*?$", folder_name):
                errors.append(Exception("教师姓名识别失败", folder_name))
            return None

        return [matcher.group(1), matcher.group(2), matcher.group(3)]

    def test_regex_match(self, regex: str, file_name: str) -> bool:
        """
        判断字符串是否匹配给定正则

        Args:
            regex (str): 正则表达式
            file_name (str): 文件名或字符串

        Returns:
            bool: 匹配返回 True，否则 False
        """
        return re.search(regex, file_name) is not None

    def get_material_by_teaching_plan(self,
                                      file: Path, material_type: str, file_name: str, fields: List[str],
                                      semester: str, errors: List[Exception]
                                      ) -> Optional[TeachingMaterialSubmit]:
        """
        获取教案材料对象
        """
        week = self.get_week_by_file_name(file_name)
        if week is None:
            errors.append(Exception(file_name, "教学周识别失败"))
            return None

        course_name, class_name, teacher_name = fields

        teacher_id = self.user_query.get_id_by_teacher_name(teacher_name)
        if teacher_id is None:
            errors.append(Exception(file_name, "教师姓名错误或数据库识别教师姓名失败"))
            return None

        course_id = self.course_query.get_course_id_by_course_name(course_name)
        if course_id is None:
            errors.append(Exception(file_name, "课程名称错误或数据库识别课程名称失败"))
            return None

        tm = TeachingMaterialSubmit()
        tm.teacher_id = teacher_id
        tm.course_id = course_id
        tm.class_name = class_name
        tm.semester = semester
        tm.week = week
        tm.material_type = material_type
        tm.file_path = str(file)

        # 规范教案命名
        self.specify_teaching_plan_naming(file, semester, course_name, class_name, teacher_name, week)

        return tm

    def specify_teaching_plan_naming(self,
                                     file: Path, semester: str, course_name: str, class_name: str, teacher_name: str,
                                     week: int
                                     ) -> None:
        """
        规范教案文件命名
        """
        file_format = self.get_file_format(file)
        correct_file_name = f"{semester}教案《{course_name}》{class_name}_{teacher_name}_第{week}周.{file_format}"

        source_path = file
        parent_dir = source_path.parent
        target_path = parent_dir / correct_file_name

        if target_path.exists():
            return

        source_path.rename(target_path)

    def save_if_not_exists(self, material: TeachingMaterialSubmit) -> None:
        """
        如果数据库中不存在该材料，则保存
        """
        existing = self.material_query.find_by_teacher_name_and_course_name_and_class_name_and_semester_and_week_and_material_type(
            material.teacher_name,
            material.course_name,
            material.class_name,
            material.semester,
            material.week,
            material.material_type
        )
        if existing is None:
            self.material_query.save(material)

    def get_teaching_material_by_file(self,
                                      file: Path, material_type: str, errors: List[Exception]
                                      ) -> Optional[TeachingMaterialSubmit]:
        """
        根据文件获取对应的材料对象
        """
        file_name = file.name

        if material_type == "teaching_situation":
            return self.get_material_by_teaching_situation(file, material_type, file_name, errors)
        elif material_type == "score_report":
            return self.get_material_by_score_report(file, material_type, file_name, errors)
        elif material_type == "training_plan":
            return self.get_material_by_training_plan(file, material_type, file_name, errors)
        elif material_type == "lesson_plan":
            return self.get_material_by_lesson_plan(file, material_type, file_name, errors)

        return None

    def get_material_by_teaching_situation(self,
                                           file: Path, material_type: str, file_name: str,
                                           errors: List[Exception]
                                           ) -> Optional[TeachingMaterialSubmit]:
        """
        获取教师教学工作情况材料对象
        """
        regex = r"^(.*?)教师教学工作情况考核表_(.*?)\.(doc|docx)$"
        matcher = re.match(regex, file_name)

        if not matcher:
            if not self.test_regex_match(r"^.*?教师教学工作情况考核表_", file_name):
                errors.append(Exception(file_name, "学期识别失败"))
            elif not self.test_regex_match(r"_.*?\.(doc|docx)$", file_name):
                errors.append(Exception(file_name, "教师姓名识别失败"))
            else:
                errors.append(Exception(file_name, "原因未知，需要手动检查"))
            return None

        semester, teacher_name = matcher.groups()[:2]

        teacher_id = self.user_query.get_id_by_teacher_name(teacher_name)
        if teacher_id is None:
            errors.append(Exception(file_name, "教师姓名错误或数据库识别教师姓名失败"))
            return None

        # 课程 ID 对于该类型文件一般不需要，可填 None 或特殊标记
        tm = TeachingMaterialSubmit()
        tm.teacher_id = teacher_id
        tm.course_id = None
        tm.class_name = None
        tm.semester = semester
        tm.week = -1
        tm.material_type = material_type
        tm.file_path = str(file)

        return tm

    def get_material_by_score_report(self,
                                     file: Path, material_type: str, file_name: str,
                                     errors: List[Exception]
                                     ) -> Optional[TeachingMaterialSubmit]:
        """
        获取课程成绩报告单材料对象
        """
        regex = r"^(.*?)课程成绩报告单及教学质量分析表《(.*?)》_(.*?)_(.*?)\.(xls|xlsx)$"
        matcher = re.match(regex, file_name)

        if not matcher:
            if not self.test_regex_match(r"^.*?课程成绩报告单及教学质量分析表《", file_name):
                errors.append(Exception(file_name, "学期识别失败"))
            elif not self.test_regex_match(r"课程成绩报告单及教学质量分析表《.*?》", file_name):
                errors.append(Exception(file_name, "课程识别失败"))
            elif not self.test_regex_match(r"》_.*?_", file_name):
                errors.append(Exception(file_name, "班级识别失败"))
            elif not self.test_regex_match(r"_.*?\.(xls|xlsx)$", file_name):
                errors.append(Exception(file_name, "教师姓名识别失败"))
            else:
                errors.append(Exception(file_name, "原因未知，需要手动检查"))
            return None

        semester, course_name, class_name, teacher_name = matcher.groups()[:4]

        teacher_id = self.user_query.get_id_by_teacher_name(teacher_name)
        if teacher_id is None:
            errors.append(Exception(file_name, "教师姓名错误或数据库识别教师姓名失败"))
            return None

        course_id = self.course_query.get_course_id_by_course_name(course_name)
        if course_id is None:
            errors.append(Exception(file_name, "课程名称错误或数据库识别课程名称失败"))
            return None

        tm = TeachingMaterialSubmit()
        tm.teacher_id = teacher_id
        tm.course_id = course_id
        tm.class_name = class_name
        tm.semester = semester
        tm.week = -1
        tm.material_type = material_type
        tm.file_path = str(file)

        return tm

    def get_material_by_training_plan(self,
                                      file: Path, material_type: str, file_name: str,
                                      errors: List[Exception]
                                      ) -> Optional[TeachingMaterialSubmit]:
        """
        获取实训计划材料对象
        """
        regex = r"^(.*?)实训计划《(.*?)》_(.*?)_(.*?)\.(xls|xlsx)$"
        matcher = re.match(regex, file_name)

        if not matcher:
            if not self.test_regex_match(r"^.*?实训计划《", file_name):
                errors.append(Exception(file_name, "学期识别失败"))
            elif not self.test_regex_match(r"实训计划《.*?》", file_name):
                errors.append(Exception(file_name, "课程识别失败"))
            elif not self.test_regex_match(r"》_.*?_", file_name):
                errors.append(Exception(file_name, "班级识别失败"))
            elif not self.test_regex_match(r"_.*?\.(xls|xlsx)$", file_name):
                errors.append(Exception(file_name, "教师姓名识别失败"))
            else:
                errors.append(Exception(file_name, "原因未知，需要手动检查"))
            return None

        semester, course_name, class_name, teacher_name = matcher.groups()[:4]

        teacher_id = self.user_query.get_id_by_teacher_name(teacher_name)
        if teacher_id is None:
            errors.append(Exception(file_name, "教师姓名错误或数据库识别教师姓名失败"))
            return None

        course_id = self.course_query.get_course_id_by_course_name(course_name)
        if course_id is None:
            errors.append(Exception(file_name, "课程名称错误或数据库识别课程名称失败"))
            return None

        tm = TeachingMaterialSubmit()
        tm.teacher_id = teacher_id
        tm.course_id = course_id
        tm.class_name = class_name
        tm.semester = semester
        tm.week = -1
        tm.material_type = material_type
        tm.file_path = str(file)

        return tm

    def get_material_by_lesson_plan(self,
                                    file: Path, material_type: str, file_name: str,
                                    errors: List[Exception]
                                    ) -> Optional[TeachingMaterialSubmit]:
        """
        获取授课计划材料对象
        """
        regex = r"^(.*?)授课计划《(.*?)》_(.*?)_(.*?)\.(doc|docx)$"
        matcher = re.match(regex, file_name)

        if not matcher:
            if not self.test_regex_match(r"^.*?授课计划《", file_name):
                errors.append(Exception(file_name, "学期识别失败"))
            elif not self.test_regex_match(r"授课计划《.*?》", file_name):
                errors.append(Exception(file_name, "课程识别失败"))
            elif not self.test_regex_match(r"》_.*?_", file_name):
                errors.append(Exception(file_name, "班级识别失败"))
            elif not self.test_regex_match(r"_.*?\.(doc|docx)$", file_name):
                errors.append(Exception(file_name, "教师姓名识别失败"))
            else:
                errors.append(Exception(file_name, "原因未知，需要手动检查"))
            return None

        semester, course_name, class_name, teacher_name = matcher.groups()[:4]

        teacher_id = user_query.get_id_by_teacher_name(teacher_name)
        if teacher_id is None:
            errors.append(Exception(file_name, "教师姓名错误或数据库识别教师姓名失败"))
            return None

        course_id = course_query.get_course_id_by_course_name(course_name)
        if course_id is None:
            errors.append(Exception(file_name, "课程名称错误或数据库识别课程名称失败"))
            return None

        tm = TeachingMaterialSubmit()
        tm.teacher_id = teacher_id
        tm.course_id = course_id
        tm.class_name = class_name
        tm.semester = semester
        tm.week = -1
        tm.material_type = material_type
        tm.file_path = str(file)

        return tm

    def get_file_format(self, file: Path) -> str:
        """
        获取文件后缀名，例如 docx、xlsx
        """
        return file.suffix.lstrip(".")

    def get_merge_material_class_list(self, file_name: str) -> List[str]:
        split_teacher = file_name.split("_")
        return split_teacher[1].split(",")

    def get_merge_material_teacher_list(self, file_name: str) -> List[str]:
        split_teacher = file_name.split("_")
        return split_teacher[2].split(",")

    # 将临时目录里的文件提交到工作目录
    def submit_tempdir_to_workdir(self):
        for file in Path(ENV_CONFIG.TEMP_PATH).rglob("*"):
            if file.is_file():
                # 先得到相对路径和绝对路径
                relative_path = file.relative_to(ENV_CONFIG.TEMP_PATH)
                dst_path = ENV_CONFIG.BASE_PATH / relative_path
                try:
                    # 将临时目录中的文件，存入到工作目录中，由于在存储到临时目录时已经做好了命名规范，所以这边不需要再去做
                    # 1. 先创建目录
                    if not dst_path.parent.exists():
                        dst_path.parent.mkdir(parents=True, exist_ok=True)
                    # 2. 复制文件内容到目标路径
                    shutil.copy2(file, dst_path)

                except Exception as e:
                    raise ResourceNotFoundError(extra_details={"u-facingMsg": f"复制{file}到{dst_path}失败：{e}"})
        # 然后将整个临时目录备份一份到备份目录中，创建一个目录在备份目录下，命名为年月日
        today_str = datetime.now().strftime("%Y年%m月%d日")
        backup_dir = ENV_CONFIG.BACKUP_PATH / today_str

        try:
            # 创建今天的备份目录
            backup_dir.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            raise ResourceNotFoundError(extra_details={"u-facingMsg": f"创建备份目录失败：{backup_dir},{e}"})

        try:
            # 把整个临时目录复制过去到今天的备份目录
            shutil.copytree(ENV_CONFIG.TEMP_PATH, backup_dir, dirs_exist_ok=True)
        except Exception:
            raise ResourceNotFoundError(extra_details={"u-facingMsg": "复制失败"})

        # 清空临时目录下的文件和子目录，但保留TEMP_PATH
        for item in ENV_CONFIG.TEMP_PATH.iterdir():
            try:
                if item.is_file():
                    item.unlink()  # 删除文件
                elif item.is_dir():
                    shutil.rmtree(item)  # 删除子目录
            except Exception:
                raise ResourceNotFoundError(extra_details={"u-facingMsg": "清空临时目录失败"})

    # 包含完整路径的文件名
    def remove_file(self, full_path: str):
        try:
            if os.path.exists(full_path):
                os.remove(full_path)
        except Exception as e:
            raise InternalServerError()

file = Files()